/*******************************************************************************
 * Copyright 2011 Mauro Luigi Drago (drago.luigi@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.bytenose.extrautils.collect;

import com.bytenose.extrautils.annotations.NonNullable;
import com.bytenose.extrautils.functional.Predicate2;

import com.google.common.base.*;

/**
 * A generic wrapper for objects providing custom {@link Object#equals(Object)} 
 * and {@link Object#hashCode()} implementations.
 * Equality and hashing are delegated to an external predicate and function, respectively, 
 * which are specified on wrapper construction.
 * Two wrapper objects are equal if the wrapped objects are equal according 
 * to the external equals predicate.
 * The predicate and function implementing {@link Object#equals(Object)} 
 * and {@link Object#hashCode()} used by this wrapper must satisfy their respective
 * contracts as specified in the java API.
 *  
 * @author Mauro Luigi Drago
 *
 * @param <T> the type of the object to wrap.
 */
final class ExternalEqualityWrapper<T> {
	/** The wrapped object */
	private final T wrappedObject;
	
	/** The predicate implementing {@link Object#equals(Object)}*/
	private final Predicate2<T,T> equals;
	
	/** The function implementing {@link Object#hashCode()} */
	private final Function<T,Integer> hashCode;
	
	ExternalEqualityWrapper(
			@NonNullable T decoratedObject, 
			@NonNullable Predicate2<T,T> equals, 
			@NonNullable Function<T,Integer> hashCode) 
			throws IllegalArgumentException {
		if (decoratedObject == null) throw new IllegalArgumentException("decoratedObject");
		if (equals == null) throw new IllegalArgumentException("equals");
		if (hashCode == null) throw new IllegalArgumentException("hashCode");
		
		this.wrappedObject = decoratedObject;
		this.equals = equals;
		this.hashCode = hashCode;
	}
	
	@Override
	public int hashCode() {
		return hashCode.apply(wrappedObject);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ExternalEqualityWrapper<?> other = (ExternalEqualityWrapper<?>) obj;
		
		// Chech that decorated objects match
		if (other.wrappedObject == null) 
			return equals.apply(wrappedObject, null);
		
		if (wrappedObject != null) {
			// We can handle this case safely without using the nasty
			// ClassCastException catch block
			if (wrappedObject.getClass().isAssignableFrom(other.wrappedObject.getClass())) {
				/*
				 * This is safe, we checked it with the previous condition. 
				 */
				@SuppressWarnings("unchecked")
				T otherDecoratedObject2 = (T) other.wrappedObject;
				return equals.apply(wrappedObject, otherDecoratedObject2);
			} else {
				// The types are not compatible
				return false;
			}
		}
		
		// We end up here when this wrapped object is null, hence we cannot do the safety
		// check we did before
		try {
			/*
			 * This is ok, we do this cast on purpose and we handle bad situations
			 * in the catch block. 
			 */
			@SuppressWarnings("unchecked")
			T otherDecoratedObject2 = (T) other.wrappedObject;
			return equals.apply(wrappedObject, otherDecoratedObject2);
		} catch (ClassCastException e) {
			// If we end up here, the other object is not of the same type.
			return false;
		}
	}
	
	/**
	 * Gets the wrapped object.
	 * @return the object
	 */
	@NonNullable
	public T getWrappedObject() {
		return wrappedObject;
	}
}
